﻿using System;
using System.Drawing;
using Microsoft.WowAddonStudio.FrameXml.Serialization;
using Microsoft.WowAddonStudio.FrameXml.Serialization.LayoutFrames;
using Microsoft.WowAddonStudio.FrameXml.Serialization.Support;
using Microsoft.WowAddonStudio.FrameXml.Services;
using Font = System.Drawing.Font;
using FrameXmlFont = Microsoft.WowAddonStudio.FrameXml.Serialization.Font;
using FrameXmlColor = Microsoft.WowAddonStudio.FrameXml.Serialization.Color;

namespace Microsoft.WowAddonStudio.FrameXml.Components.Support
{
    public class FrameXmlGraphics
    {
        private readonly FrameXmlImageService imageService;
        private readonly FrameXmlFontService fontService;
        private readonly Graphics graphics;

        /// <summary>
        /// Initializes a new instance of the <see cref="FrameXmlGraphics"/> class.
        /// </summary>
        /// <param name="imageService">The FrameXML image service.</param>
        /// <param name="fontService">The FrameXML font service.</param>
        /// <param name="graphics">The graphics context.</param>
        public FrameXmlGraphics(FrameXmlImageService imageService, FrameXmlFontService fontService, Graphics graphics)
        {
            if (imageService == null)
                throw new ArgumentNullException("imageService");
            if (fontService == null)
                throw new ArgumentNullException("fontService");
            if (graphics == null)
                throw new ArgumentNullException("graphics");

            this.imageService = imageService;
            this.fontService = fontService;
            this.graphics = graphics;
        }

        /// <summary>
        /// Gets the <see cref="Graphics"/> used to paint.
        /// </summary>
        public Graphics Graphics
        {
            get { return graphics; }
        }

        #region Backdrop

        /// <summary>
        /// Draws the backdrop.
        /// </summary>
        /// <param name="backdrop">The backdrop.</param>
        /// <param name="rect">The rect.</param>
        public virtual bool DrawBackdrop(Backdrop backdrop, Rectangle rect)
        {
            if (backdrop == null)
                throw new ArgumentNullException("backdrop");

            if (backdrop.IsComplete)
            {
                // Translate the coordinate system to simplify the rendering of the backdrop elements
                graphics.TranslateTransform(rect.Left, rect.Top);

                try
                {
                    var isEdgeDrawingSuccess = false;
                    var isBackgroundDrawingSuccess = false;

                    if (backdrop.EdgeFile.IsAvailable())
                        isEdgeDrawingSuccess = DrawBackdropEdge(backdrop, rect.Size);

                    if (backdrop.BackgroundFile.IsAvailable())
                        isBackgroundDrawingSuccess = DrawBackdropBackground(backdrop, rect.Size);

                    if (!isEdgeDrawingSuccess && !isBackgroundDrawingSuccess)
                        return false;

                    return true;
                }
                finally
                {
                    // Reset the transformation back
                    graphics.ResetTransform();
                }
            }

            return false;
        }

        /// <summary>
        /// Draws the backdrop edge.
        /// </summary>
        /// <param name="backdrop">The backdrop.</param>
        /// <param name="size">The size.</param>
        private bool DrawBackdropEdge(Backdrop backdrop, Size size)
        {
            Image edgeImage = imageService.GetImage(backdrop.EdgeFile);
            if (edgeImage == null)
                return false;

            int edgeSize = 32;
            if (backdrop.EdgeSize.HasValue)
                edgeSize = backdrop.EdgeSize.Value;

            var edgeSliceSize = new Size(edgeImage.Width / 8, edgeImage.Height);

            // Draw corners
            foreach (var corner in new[] { Corner.TopLeft, Corner.BottomLeft, Corner.TopRight, Corner.BottomRight })
            {
                var srcRect = GetBackdropSourceRectangle(edgeSliceSize, (int)corner);
                var destRect = GetBackdropCornerDestinationRectangle(size, edgeSize, corner);

                graphics.DrawImage(edgeImage, destRect, srcRect, GraphicsUnit.Pixel);
            }

            // Draw horizontal edges
            foreach (var edge in new[] { Edge.Top, Edge.Bottom })
            {
                var srcRect = GetBackdropSourceRectangle(edgeSliceSize, (int)edge);

                for (int x = edgeSize; x < size.Width - edgeSize; x += edgeSize)
                {
                    // Depending on whether this is a Top or Bottom edge, determine y
                    int y = edge == Edge.Top ? 0 : size.Height - edgeSize;

                    // Account for partial edge that shouldn't draw into the corner's area
                    int width = Math.Min(edgeSize, size.Width - edgeSize - x);

                    // Create destination points (rotate 90 degrees CCW)
                    var topLeft = new Point(x + width, y);
                    var topRight = new Point(x + width, y + edgeSize);
                    var bottomLeft = new Point(x, y);

                    // Account for single pixel issue when drawing partial tile
                    if (width != edgeSize)
                        bottomLeft.X = bottomLeft.X - 1;

                    // Create array holding the destination points
                    var destPoints = new[] { topLeft, topRight, bottomLeft };

                    // Draw the image
                    graphics.DrawImage(edgeImage, destPoints, srcRect, GraphicsUnit.Pixel);
                }
            }

            // Draw vertical edges
            foreach (var edge in new[] { Edge.Left, Edge.Right })
            {
                var srcRect = GetBackdropSourceRectangle(edgeSliceSize, (int)edge);

                for (int y = edgeSize; y < size.Height - edgeSize; y += edgeSize)
                {
                    // Depending on whether this is a Left or Right edge, determine x
                    int x = edge == Edge.Left ? 0 : size.Width - edgeSize;

                    // Account for partial edge that shouldn't draw into the corner's area
                    int height = Math.Min(edgeSize, size.Height - edgeSize - y);

                    // Create destination rectangle
                    var destRect = new Rectangle(new Point(x, y), new Size(edgeSize, height));

                    // Draw the image
                    graphics.DrawImage(edgeImage, destRect, srcRect, GraphicsUnit.Pixel);
                }
            }

            return true;
        }

        /// <summary>
        /// Draws the backdrop background.
        /// </summary>
        /// <param name="backdrop">The backdrop.</param>
        /// <param name="size">The size.</param>
        private bool DrawBackdropBackground(Backdrop backdrop, Size size)
        {
            Image backgroundImage = imageService.GetImage(backdrop.BackgroundFile);
            if (backgroundImage == null)
                return false;

            Inset inset = backdrop.BackgroundInset ?? new Inset();

            if (backdrop.Tile)
            {
                int tileSize = 32;
                if (backdrop.TileSize.HasValue)
                    tileSize = backdrop.TileSize.Value;

                for (int x = inset.Left; x < size.Width - inset.Right; x += tileSize)
                    for (int y = inset.Top; y < size.Height - inset.Bottom; y += tileSize)
                    {
                        // Account for partial tiles along the right and bottom edges
                        int width = Math.Min(tileSize, size.Width - inset.Right - x);
                        int height = Math.Min(tileSize, size.Height - inset.Bottom - y);

                        // Draw the background image
                        graphics.DrawImage(backgroundImage, x, y, width, height);
                    }
            }
            else
            {
                // Draw background stretched across the background area
                graphics.DrawImage(backgroundImage, inset.Left,
                                   inset.Top,
                                   size.Width - inset.Left - inset.Right,
                                   size.Height - inset.Top - inset.Bottom);
            }

            return true;
        }

        /// <summary>
        /// Gets the backdrop source rectangle.
        /// </summary>
        /// <param name="edgeSliceSize">Size of the edge slice.</param>
        /// <param name="edgeSliceIndex">Index of the edge slice.</param>
        /// <returns></returns>
        private static Rectangle GetBackdropSourceRectangle(Size edgeSliceSize, int edgeSliceIndex)
        {
            return new Rectangle(new Point(edgeSliceIndex * edgeSliceSize.Width, 0), edgeSliceSize);
        }

        /// <summary>
        /// Gets the backdrop corner destination rectangle.
        /// </summary>
        /// <param name="size">The size.</param>
        /// <param name="edgeSize">Size of the edge.</param>
        /// <param name="corner">The corner.</param>
        /// <returns></returns>
        private static Rectangle GetBackdropCornerDestinationRectangle(Size size, int edgeSize, Corner corner)
        {
            Point origin;

            switch (corner)
            {
                case Corner.TopLeft:
                    origin = new Point(0, 0);
                    break;
                case Corner.BottomLeft:
                    origin = new Point(0, size.Height - edgeSize);
                    break;
                case Corner.TopRight:
                    origin = new Point(size.Width - edgeSize, 0);
                    break;
                case Corner.BottomRight:
                    origin = new Point(size.Width - edgeSize, size.Height - edgeSize);
                    break;
                default:
                    throw new ArgumentException("Invalid corner.", "corner");
            }

            return new Rectangle(origin, new Size(edgeSize, edgeSize));
        }

        #endregion

        #region FontString

        /// <summary>
        /// Draws the font string.
        /// </summary>
        /// <param name="fontString">The font string.</param>
        /// <param name="rect">The rect.</param>
        public virtual bool DrawFontString(FontString fontString, Rectangle rect)
        {
            if (!fontString.IsComplete)
                return false;

            // Retrieve the font family from the font service
            FontFamily fontFamily = fontService.GetFont(fontString.Font);
            if (fontFamily == null)
                return false;

            // Create the font from the font family, the height of the fontstring and the font style
            using (var font = new Font(fontFamily, (int)fontString.FontHeight.Value, FontStyle.Regular, GraphicsUnit.Pixel))
            {
                // Check if we have a shadow and it is complete
                if (fontString.Shadow != null && fontString.Shadow.IsComplete)
                {
                    // Draw the shadow
                    var shadowColor = fontString.Shadow.Color.ToColor();

                    // Offset the shadow rectangle
                    var shadowRect = rect;
                    shadowRect.Offset(fontString.Shadow.Offset.ToPoint());

                    // Draw the string according to the font, shadow color and string format
                    graphics.DrawString(fontString.Text, font, new SolidBrush(shadowColor), shadowRect, fontString.StringFormat);
                }

                // Convert the color
                var color = fontString.Color.ToColor();

                // Draw the string according to the font, color and string format
                graphics.DrawString(fontString.Text, font, new SolidBrush(color), rect, fontString.StringFormat);
            }

            return true;
        }

        /// <summary>
        /// Draws the font string.
        /// </summary>
        /// <param name="fontString">The font string.</param>
        /// <param name="font">The font.</param>
        /// <param name="color">The color.</param>
        /// <param name="text">The text.</param>
        /// <param name="rect">The rect.</param>
        public virtual bool DrawFontString(FontString fontString, FrameXmlFont font, FrameXmlColor color, string text,
                                           Rectangle rect)
        {
            if (fontString == null)
                throw new ArgumentNullException("fontString");

            // Determine actual values to use
            var fontPath = fontString.Font ?? (font != null ? font.TrueTypeFont : null);
            var fontHeight = fontString.FontHeight ?? (font != null ? font.FontHeight : null);
            var fontColor = fontString.Color ?? (color ?? (font != null ? font.Color : null));
            var fontText = text ?? fontString.Text;
            var fontShadow = fontString.Shadow ?? (font != null ? font.Shadow : null);


            // Draw a temporary fontstring created by the actual values
            return DrawFontString(new FontString
                            {
                                Text = fontText,
                                Font = fontPath,
                                FontHeight = fontHeight,
                                Color = fontColor,
                                Shadow = fontShadow,
                                JustifyVertical = JustifyVertical.Middle,
                                JustifyHorizontal = fontString.JustifyHorizontal
                            }, rect);
        }

        /// <summary>
        /// Draws the font string.
        /// </summary>
        /// <param name="fontString">The font string.</param>
        /// <param name="font">The font.</param>
        /// <param name="color">The color.</param>
        /// <param name="rect">The rect.</param>
        public virtual bool DrawFontString(FontString fontString, FrameXmlFont font, FrameXmlColor color, Rectangle rect)
        {
            return DrawFontString(fontString, font, color, null, rect);
        }

        #endregion

        #region Texture

        /// <summary>
        /// Draws the texture.
        /// </summary>
        /// <param name="texture">The texture.</param>
        /// <param name="rect">The rect.</param>
        public virtual bool DrawTexture(Texture texture, Rectangle rect)
        {
            if (!texture.IsComplete)
                return false;

            // If texture file exists use it for texture drawing.
            if (!String.IsNullOrEmpty(texture.File))
            {
                // Get the image and check if it was successfully loaded
                Image image = imageService.GetImage(texture.File);
                if (image == null)
                    return false;

                if (texture.TexCoords == null)
                {
                    graphics.DrawImage(image, rect);
                }
                else
                {
                    // Calculate the source coordinates and size from the texture coordinates and the width of the image
                    var srcX = texture.TexCoords.Left * image.Width;
                    var srcY = texture.TexCoords.Top * image.Height;
                    var srcWidth = (texture.TexCoords.Right - texture.TexCoords.Left) * image.Width;
                    var srcHeight = (texture.TexCoords.Bottom - texture.TexCoords.Top) * image.Height;

                    // Draw the image using the calculated values
                    graphics.DrawImage(image, rect, srcX, srcY, srcWidth, srcHeight, GraphicsUnit.Pixel);
                }
            }
            else
            {
                // If there is no image for texture use texture color for drawing.
                Pen pen = new Pen(texture.Color.ToColor());
                graphics.DrawRectangle(pen, rect);
                graphics.FillRectangle(pen.Brush, rect);
            }

            return true;
        }

        #endregion

        #region Button

        /// <summary>
        /// Draws the button.
        /// </summary>
        /// <param name="button">The button.</param>
        /// <param name="buttonState">State of the button.</param>
        /// <param name="rect">The rect.</param>
        public virtual bool DrawButton(Button button, ButtonState buttonState, Rectangle rect)
        {
            FrameXmlFont font = null;

            // Get the texture, font and color for the current state
            var texture = button.GetStateTexture(buttonState);
            var style = button.GetStateStyle((buttonState));
            var color = button.GetStateColor((buttonState));

            bool isDrawnFontStringSuccess = false;
            bool isDrawnTextureSuccess = false;

            // If we have a texture, draw texture
            if (texture != null)
                isDrawnTextureSuccess = DrawTexture(texture, rect);

            // If we have a style, get font
            if (style != null && style.IsComplete)
                font = FrameXmlGraphics.GetFont(style);

            if (font != null)
            {
                // Adjust the client rectangle to better position the text
                var textRectangle = rect;

                textRectangle.Y += 5;
                textRectangle.Height -= 5;

                if (button.ButtonText != null)
                    isDrawnFontStringSuccess = DrawFontString(button.ButtonText, font, color, button.Text, textRectangle);
                else
                    isDrawnFontStringSuccess = DrawFontString(new FontString { Text = button.Text }, font, color, textRectangle);
            }

            if (!isDrawnTextureSuccess && !isDrawnFontStringSuccess)
                return false;

            return true;
        }

        private static FrameXmlFont GetFont(ButtonStyle buttonStyle)
        {
            // Look up the font with the given name (style)
            if (buttonStyle.IsComplete && FrameXmlDirectory.Default.IsRegistered<FrameXmlFont>(buttonStyle.Style))
                return FrameXmlDirectory.Default.Resolve<FrameXmlFont>(buttonStyle.Style);

            return null;
        }

        #endregion

        #region Placeholder

        /// <summary>
        /// Draws the placeholder.
        /// </summary>
        /// <param name="layoutFrame">The layout frame.</param>
        /// <param name="font">The font.</param>
        /// <param name="rectangle">The rectangle.</param>
        public void DrawPlaceholder(LayoutFrame layoutFrame, Font font, Rectangle rectangle)
        {
            var drawRectangle = new Rectangle(rectangle.Location,
                                              new Size(rectangle.Width - 1, rectangle.Height - 1));

            // Draw a black rectangle
            graphics.DrawRectangle(Pens.Black, drawRectangle);
        }

        #endregion
    }
}